/*
 * #%L
 * Tools
 * %%
 * Copyright (C) 2011 Ractoc.com
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */
package com.ractoc.tools.codegenerators.tasks;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Task;

import com.ractoc.tools.codegenerators.GenerateBeanTranslator;
import com.ractoc.tools.codegenerators.GenerateBeanTranslator.Translator;

/**
 * Task which is called from ant to generate bean translators.
 * 
 * @author ractoc
 * @version 1.0
 */
public final class GenerateBeanTranslatorTask extends Task {

    private static final String UNABLE_TO_CREATE_PACKAGE_DIR = "Unable to create package dir.";

    private String destDir;

    private List<Bean> beans = new ArrayList<Bean>();

    private String mapPackage;

    private Boolean verbose;

    private String classpath;

    private ClassLoader loader;
    
    /**
     * Constructor.
     */
    public GenerateBeanTranslatorTask() {
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void execute() {

        printVerbose("generator classpath: " + classpath);
        
        if (classpath == null || classpath.length() == 0) {
            throw new BuildException("No classpath provided");
        }

        try {
            loadClassPath();
        } catch (final MalformedURLException e) {
            throw new BuildException(e);
        }

        final File dir = new File(destDir + File.separator + mapPackage.replace('.', File.separatorChar));
        printVerbose("generating files in " + dir.getPath());
        if (!dir.exists()) {
            if (!dir.mkdirs()) {
                throw new BuildException(UNABLE_TO_CREATE_PACKAGE_DIR);
            }
        }

        try {
            printVerbose("generating Translator.java");
            final File translatorInterface = new File(dir, "Translator.java");
            FileWriter writer = null;
            writer = new FileWriter(translatorInterface);
            writeTranslatorInterface(writer);

            printVerbose("generating TranslatoMap.java");
            final File translatorMapFile = new File(dir, "TranslatorMap.java");
            writer = null;

            writer = new FileWriter(translatorMapFile);
            writeTranslatorMapheaders(writer);

            writeBeans(writer);

            writeTranslatorMapFooter(writer);
            writer.close();
        } catch (final ClassNotFoundException e) {
            throw new BuildException(e);
        } catch (final IOException e) {
            throw new BuildException(e);
        }
    }

    private void loadClassPath() throws MalformedURLException {
        String[] entries = classpath.split(File.pathSeparator);
        final URL[] urls = new URL[entries.length];
        for (int i = 0; i < entries.length; i++) {
            urls[i] = new File(entries[i]).toURI().toURL();
        }
        
        loader = new URLClassLoader(urls);
    }

    private void printVerbose(final String message) {
        if (isVerbose()) {
            System.out.println(message);
        }
    }

    private void writeBeans(final FileWriter writer) throws ClassNotFoundException, IOException {
        for (Bean bean : beans) {
            printVerbose("beanIn: " + bean.getBeanIn());
            printVerbose("beanOut: " + bean.getBeanOut());
            final Class beanInClass = loader.loadClass(bean.getBeanIn());
            final Class beanOutClass = loader.loadClass(bean.getBeanOut());
            Translator translator = GenerateBeanTranslator.convert(beanInClass, beanOutClass, bean.getPackageName(),
                    mapPackage, verbose);
            storeTranslator(bean.getPackageName(), translator);
            writer.write("        translatorsByKey.put(\"" + beanInClass.getName() + "\",\n");
            writer.write("                        new " + bean.getPackageName() + "." + translator.getTranslatorName()
                    + "());\n");
            if (bean.isInverted()) {
                translator = GenerateBeanTranslator.convert(beanOutClass, beanInClass, bean.getPackageName(),
                        mapPackage, verbose);
                storeTranslator(bean.getPackageName(), translator);
                writer.write("        translatorsByKey.put(\"" + beanOutClass.getName() + "\",\n");
                writer.write("                        new " + bean.getPackageName() + "."
                        + translator.getTranslatorName() + "());\n");
            }
        }
    }

    private void writeTranslatorInterface(final FileWriter writer) throws IOException {
        writer.write("//CHECKSTYLE:OFF\n\n");
        writer.write("package " + mapPackage + ";\n\n");
        writer.write("/**\n");
        writer.write(" * Main translator interface. Used by the map as a uniform way to access all translators.\n");
        writer.write(" * @author Mark Schrijver\n");
        writer.write(" * @version 2.0\n");
        writer.write(" */\n");
        writer.write("public interface Translator {\n\n\t");
        writer.write("/**\n");
        writer.write(" * Translates the provided object into the registered oposite object.\n");
        writer.write(" * @param beanToTranslate The bean that needs to be translated.\n");
        writer.write(" * @return The translated bean.\n");
        writer.write(" */\n");
        writer.write("public Object translate(Object beanToTranslate);\n\n");
        writer.write("//CHECKSTYLE:ON\n\n");
        writer.write("}");
        writer.flush();
        writer.close();
    }

    private void writeTranslatorMapheaders(final FileWriter writer) throws IOException {
        writer.write("//CHECKSTYLE:OFF\n\n");
        writer.write("package " + mapPackage + ";\n\n");
        writer.write("import java.util.Map;\n");
        writer.write("import java.util.LinkedHashMap;\n");
        writer.write("/**\n");
        writer.write(" * Main translator class. Only has one method, translate.\n");
        writer.write(" * @author Mark Schrijver\n");
        writer.write(" * @version 1.0\n");
        writer.write(" */\n");
        writer.write("public final class TranslatorMap {\n\n");
        writer.write("    private static Map <String, Translator> translatorsByKey = "
                + "new LinkedHashMap <String, Translator>();\n\n");
        writer.write("    static {\n");
    }

    private void writeTranslatorMapFooter(final FileWriter writer) throws IOException {
        writer.write("    }\n\n");
        writer.write("    /**\n");
        writer.write("     * Constructor.\n");
        writer.write("     */\n");
        writer.write("    private TranslatorMap() {\n");
        writer.write("    }\n\n");
        writer.write("    /**\n");
        writer.write("     * Selects the correct translator and uses that translator to translate the"
                + " supplied message.\n");
        writer.write("     * Only one translator is possible per bean class.\n"
                + "     * If no translator is available null is returned.\n");
        writer.write("     * @param beanToTranslate the bean that needs to be translated.\n");
        writer.write("     * @return the translated bean or null if no translator was available.\n");
        writer.write("     */\n");
        writer.write("    public static Object translate(final Object beanToTranslate) {\n");
        writer.write("        Object result = null;\n");
        writer.write("        if (beanToTranslate != null) {\n");
        writer.write("            final Translator translator = translatorsByKey."
                + "get(beanToTranslate.getClass().getName());\n");
        writer.write("            if (translator != null) {\n");
        writer.write("                result = translator.translate(beanToTranslate);\n");
        writer.write("            }\n");
        writer.write("        }\n");
        writer.write("        return result;\n");
        writer.write("    }\n}\n\n");
        writer.write("//CHECKSTYLE:ONFF\n\n");
    }

    private void storeTranslator(final String packageName, final Translator translator) throws IOException {
        if (packageName == null || packageName.length() == 0) {
            throw new IOException("No package name specified");
        }
        final File dir = new File(destDir + "/" + packageName.replace('.', '/'));
        if (!dir.exists()) {
            if (!dir.mkdirs()) {
                throw new IOException(UNABLE_TO_CREATE_PACKAGE_DIR);
            }
        }
        final File translatorFile = new File(dir, translator.getTranslatorName() + ".java");
        final FileWriter writer = new FileWriter(translatorFile);
        writer.write(translator.getTranslatorContent());
        writer.close();
    }

    /**
     * Sets the destination directory.
     * 
     * @param destDir
     *            directory.
     */
    public void setDestDir(final String destDir) {
        this.destDir = destDir;
    }

    /**
     * Sets the map package.
     * 
     * @param mapPackage
     *            the map package..
     */
    public void setMapPackage(final String mapPackage) {
        this.mapPackage = mapPackage;
    }

    /**
     * Creates the bean config based on the sub-tag.
     * 
     * @return the bean created.
     */
    public Bean createBean() {
        final Bean bean = new Bean();
        beans.add(bean);
        return bean;
    }

    /**
     * Sets the verbose value. Setting the task to verbose prints all manner of
     * output to the console.
     * 
     * @param verboseParam
     *            The value of the verbose parameter.
     */
    public void setVerbose(final String verboseParam) {
        this.verbose = Boolean.valueOf(verboseParam);
    }

    /**
     * Has the verbose parameter been set to true? Setting the task to verbose
     * prints all manner of output to the console.
     * 
     * @return The verbose value.
     */
    public boolean isVerbose() {
        return verbose.booleanValue();
    }

    public void setClasspath(String classpath) {
        this.classpath = classpath;
    }

    public String getClasspath() {
        return classpath;
    }

    /**
     * The Bean class. Contains all that is necessary to create the translator.
     * 
     * @author Schrijver
     * @version 1.0
     */
    public final class Bean {

        private String beanIn;

        private String beanOut;

        private String packageName;

        private boolean inverted;

        /**
         * Constructort.
         */
        public Bean() {
        }

        /**
         * Is this a two-way translation?
         * 
         * @return true: yes, false: no.
         */
        public boolean isInverted() {
            return inverted;
        }

        /**
         * Is this a two-way translation?
         * 
         * @param inverted
         *            true: yes, false: no.
         */
        public void setInverted(final boolean inverted) {
            this.inverted = inverted;
        }

        /**
         * Bean coming into the translator.
         * 
         * @param beanIn
         *            Bean coming into the translator.
         */
        public void setBeanIn(final String beanIn) {
            this.beanIn = beanIn;
        }

        /**
         * Bean coming out of the translator.
         * 
         * @param beanOut
         *            Bean coming out of the translator.
         */
        public void setBeanOut(final String beanOut) {
            this.beanOut = beanOut;
        }

        /**
         * The name of the package the translator is located in.
         * 
         * @param packageName
         *            The name of the package the translator is located in.
         */
        public void setPackageName(final String packageName) {
            this.packageName = packageName;
        }

        /**
         * Bean coming into the translator.
         * 
         * @return Bean coming into the translator.
         */

        public String getBeanIn() {
            return beanIn;
        }

        /**
         * Bean coming out of the translator.
         * 
         * @return Bean coming out of the translator.
         */

        public String getBeanOut() {
            return beanOut;
        }

        /**
         * The name of the package the translator is located in.
         * 
         * @return The name of the package the translator is located in.
         */
        public String getPackageName() {
            return packageName;
        }
    }
}
